﻿using System;
using System.Collections.Generic;
using System.Windows;

namespace GenericQuadTree
{
    public class QuadNode<T> where T : TreeObject
    {
        #region PRIVATE MEMBERS

        private readonly double _nodeWidth;
        private readonly double _nodeHeight;
        private readonly int _nodeDepth;

        private readonly QuadTree<T> _parentTree;
        private readonly List<T> _items = new List<T>();
        private readonly QuadNode<T> _parentNode;
        private readonly Point _location;

        #endregion

        #region PUBLIC PROPERTIES

        public int Depth { get { return _nodeDepth; } }

        public QuadNode<T>[] Children { get; private set; }

        public QuadTree<T> ParentTree
        {
            get { return _parentTree; }
        }

        public QuadNode<T> ParentNode
        {
            get { return _parentNode; }
        }

        public IList<T> Items
        {
            get { return _items.AsReadOnly(); }
        }

        public bool HasChildren { get; private set; }
        public NodeType NodeType { get; private set; }

        public double Width
        {
            get { return _nodeWidth; }
        }

        public double Height
        {
            get { return _nodeHeight; }
        }

        public Point Location
        {
            get { return _location; }
        }

        #endregion

        /// <summary>
        ///   Quad Node
        /// </summary>
        /// <param name = "nodeDepth">Depth of current node</param>
        /// <param name = "location">Point location of node (top left corner)</param>
        /// <param name = "nodeType">type of node</param>
        /// <param name = "parentTree">parent tree reference</param>
        /// <param name = "parentNode">parent node reference</param>
        public QuadNode(int nodeDepth, Point location, NodeType nodeType, QuadTree<T> parentTree, QuadNode<T> parentNode)
        {
            _nodeDepth = nodeDepth;
            _parentTree = parentTree;
            _parentNode = parentNode;
            NodeType = nodeType;
            _location = location;

            _nodeWidth = nodeDepth > 0 ? parentTree.TreeWidth/Math.Pow(2, _nodeDepth) : parentTree.TreeWidth;
            _nodeHeight = nodeDepth > 0 ? parentTree.TreeHeight/Math.Pow(2, _nodeDepth) : parentTree.TreeHeight;

            AddChildren();
        }

        private void AddChildren()
        {
            //Make sure child fits within minimum parameters before splitting this node
            if (_nodeWidth/2 < _parentTree.MinNodeWidth || _nodeHeight/2 < _parentTree.MinNodeHeight) return;
            //Need to account for the possibility that the minwidth and/or minheight are <= 0.
            //In this case, we're just going to stop splitting when our width is less than 1.
            if ((_parentTree.MinNodeHeight <= 0 && _nodeHeight < 1) ||
                (_parentTree.MinNodeWidth <= 0 && _nodeWidth < 1))
                return;

            Children = new[]
                           {
                               new QuadNode<T>(_nodeDepth + 1, _location, NodeType.NorthWest, _parentTree, this),
                               new QuadNode<T>(_nodeDepth + 1, new Point(_location.X + _nodeWidth/2, _location.Y),
                                               NodeType.NorthEast, _parentTree, this),
                               new QuadNode<T>(_nodeDepth + 1, new Point(_location.X, _location.Y + _nodeHeight/2),
                                               NodeType.SouthWest, _parentTree, this),
                               new QuadNode<T>(_nodeDepth + 1,
                                               new Point(_location.X + _nodeWidth/2, _location.Y + _nodeHeight/2),
                                               NodeType.SouthEast, _parentTree, this)
                           };

            HasChildren = true;
        }

        /// <summary>
        ///   Add a list of objects to the node.
        /// </summary>
        /// <param name = "itemList">List of objects to add</param>
        public void AddObjects(List<T> itemList)
        {
            _items.AddRange(itemList);
        }

        public void AddObject(T item)
        {
            if (_items.IndexOf(item) < 0)
                _items.Add(item);

            //Make sure item also exists for all parent nodes
            //up the tree.
            if (_parentNode != null)
                _parentNode.AddObject(item);
        }

        public void RemoveObject(T item)
        {
            _items.Remove(item);

            //Make sure item is removed from all parent nodes 
            //as well.
            if (_parentNode != null)
                _parentNode.RemoveObject(item);
        }

        internal QuadNode<T> GetNodeAtLocation(Point location, int minWidth = 0, int minHeight = 0)
        {
            if (!ContainsPoint(location))
                return null;

            if (!HasChildren)
                return this;

            //If we've reached the min width or min height for the search 
            //return this node instead of searching the children.
            if (Children[0].Width < minWidth || Children[0].Height < minHeight)
                return this;

            if (Children[0].ContainsPoint(location))
                return Children[0].GetNodeAtLocation(location);

            if (Children[1].ContainsPoint(location))
                return Children[1].GetNodeAtLocation(location);

            if (Children[2].ContainsPoint(location))
                return Children[2].GetNodeAtLocation(location);

            return Children[3].ContainsPoint(location) ? Children[3].GetNodeAtLocation(location) : null;
        }

        public bool ContainsPoint(Point location)
        {
            var maxX = _location.X + _nodeWidth;
            var maxY = _location.Y + _nodeHeight;

            return ((location.X >= _location.X && location.X <= maxX) && location.Y >= _location.Y) &&
                   location.Y <= maxY;
        }
    }
}